﻿define([
    'Wjs/core/array',
    'Wjs/core/types',
    'Wjs/core/typeManager'
],
function (array, types, typeManager) {
    'use strict';

    // Returns all the mixins that attached to the constructor of the instance o and the constructors of the base types as well
    var getAllMixins = function (ctor, output) {
        var mixins = ctor.getMixins(),
            done = false;

        if (!output) { // Assume parent will not provide the output array
            output = [];
            done = true;
        }

        if (mixins) {

            mixins.forEach(function (mixin) {
                output.push(mixin);
            });
        }

        if (ctor.base) { // Not the end of the base chain
            getAllMixins(ctor.base.constructor, output);
        }

        if (done) {
            return output;
        }
    }

    return {

        // Copies the members from "from" to "to".
        // The default action is to copy all the members without overwriting but there are options for ignoring items or overwriting then 
        // if they are specified in the ignore and overwrite arrays of the options
        copyMembers: function (from, to, options) {
            var ignore,
                overwrite;

            options = options || {};

            ignore = options.ignore || [];
            overwrite = options.overwrite || [];

            for (var name in from) {

                if (from.hasOwnProperty(name)) {

                    if (ignore.indexOf(name) > -1) { // It is in the ignore array of the configuration

                        continue;
                    }

                    if (to.hasOwnProperty(name) && // The member exists in the destination object
                        overwrite.indexOf(name) == -1 && // but it is not in the overwrite array in the options
                        overwrite.indexOf('*') == -1) { // and the overwrite "any" options was not specified

                        continue;
                    }

                    to[name] = from[name];
                }
            }
        },

        // Logs a warning into the console when there is a field in the cfg object that is ont listed in the options array
        warnIgnoredIfNot: function (cfg, options) {
            var fields = [];
            this.getFlattenedFieldNames(cfg, fields);

            for (var i = 0; i < fields.length; ++i) {
                var field = fields[i];
                if (array.indexOf(options, function (item) { return item === field; }) < 0) {
                    console.log("Warning - Configuration field: '" + field + "' is not used and will be ignored");
                }
            }
        },

        // Returns the name of the fields of the object in a flattened way (nested fields concatenated with dots)
        getFlattenedFieldNames: function (cfg, fields, prefix) {
            var fieldName;
            prefix = prefix || '';

            for (fieldName in cfg) {
                var o = cfg[fieldName];

                if (!types.isPrimitive(o) && !types.isFunction(o)) {
                    this.getFlattenedFieldNames(o, fields, prefix + fieldName + '.');
                }
                else {
                    fields.push(prefix + fieldName);
                }
            }
        },

        // Extends the derived construct method from the base one
        extend: function (derived, base) {
            derived.prototype = Object.create(base.prototype);
            derived.prototype.constructor = derived;
            derived.base = base.prototype;

            if (base.prototype.constructor == Object.prototype.constructor) {
                base.prototype.constructor = base;
            }

            // Provide a function to shortcut getting the base class
            derived.prototype.getBase = function () {
                return this.constructor.base;
            };
        },

        checkAbstractMembersAreImplemented: function (implementer, declarer) {
            var notImplemented = [];

            if (declarer.abstract) {

                declarer.abstract.forEach(function (member) {
                    if (!implementer[member]) {
                        notImplemented.push(member);
                    }
                });

                if (notImplemented.length > 0) {
                    throw 'Must implement abstract member(s): ' + notImplemented.join(', ');
                }
            }

        },

        // Creates the constructor method
        createConstructor: function (cfg) {
            var construct = cfg.construct || cfg.base || function () { },
                initialize = cfg.initialize,
                constructor = function () { // Combine all the initialization steps in the constructor
                    var args = Array.prototype.slice.call(arguments);

                    // Call the construct method for this class
                    //console.log('Calling class ' + constructor.prototype.type + '::construct on type: ' + this.type);

                    construct.apply(this, args);

                    constructor.getMixins().forEach(function (mixin) {

                        //console.log('Calling mixin ' + mixin.prototype.type + '::construct on type: ' + this.type);

                        mixin.apply(this, args); // The mixin function acts like a constructor and initializer since the type is constructred at this point
                    },
                    this);

                    // At this moment the class and all the mixins have been constructed
                    // Apply the second initialization step in which the class can access the functionality provided by the mixins as well
                    if (initialize) {

                        //console.log('Calling ' + constructor.prototype.type + '::initialize on type: ' + this.type);

                        initialize.apply(this, args);
                    }
                };

            return constructor;
        },

        copyMixinMembers: function (mixin, constructor) {
            var mixinBase = mixin.base;

            this.copyMembers(mixin.prototype, constructor.prototype, {
                ignore: ['internal', 'type']
            });

            //this.checkAbstractMembersAreImplemented(constructor.prototype, mixin.prototype);

            while (mixinBase) {

                this.copyMembers(mixinBase, constructor.prototype, {
                    ignore: ['internal', 'type']
                });

                mixinBase = mixinBase.base;
            }
        },

        // Attaches the mixins to the constructor of the host type
        attachMixins: function (constructor, mixins) {
            var allMixins;

            if (mixins) { // There are mixins to be attached to this type

                mixins.forEach(function (mixin) {
                    this.copyMixinMembers(mixin, constructor);
                },
                this);
            }

            // Copy these mambers regardless whether there are mixins or not since we need to to get the mixins from the base types as well
            // and we use getMixins for that
            this.copyMembers({

                getMixins: function () {
                    return mixins || [];
                },

                // Attach the getAllMixins function to the constructor so it can notify them about the host type events
                // This function returns the mixins attached to this type including those of the base types as well
                getAllMixins: function () {
                    return allMixins;
                },

                notifyMixins: function (listenerName, scope, cfg) {
                    var listener;

                    allMixins.forEach(function (mixin) {

                        if (mixin.listeners) {

                            listener = mixin.listeners[listenerName];

                            if (listener) {

                                //console.log('Calling mixin ' + mixin.prototype.type + '::' + listenerName + ' on type: ' + scope.type);

                                listener.call(scope, cfg);
                            }
                        }
                    });
                }
            },
            constructor);

            allMixins = getAllMixins(constructor); // Now that the constructor has the getMixins method we can populate the variable
        },

        // Creates a construct function from a configuration object
        // Common errors:
        // onBeforeTypeDefined is not handled by this function (since it is not called once). Setting it in the configuration will do nothing
        define: function (cfg) {

            var constructor,
                base,
                instance;

            if (!cfg) {
                throw 'The define method requires a configuration';
            }

            //console.log('Building type: ' + cfg.type);

            cfg.internal = cfg.internal || {};

            constructor = this.createConstructor(cfg.internal);

            // Copy the base
            base = cfg.internal.base;

            if (base) {
                this.extend(constructor, base);

                this.checkAbstractMembersAreImplemented(constructor.prototype, base.prototype);
            }

            this.copyMembers(cfg, constructor.prototype, {
                ignore: ['internal']
            });

            // If there are listeners (mainly in case of mixin types), assign them to the listeners property of the constructor
            if (cfg.internal && cfg.internal.listeners) {

                constructor.listeners = cfg.internal.listeners;
            }

            // Attach the mixins
            this.attachMixins(constructor, cfg.internal.mixins);

            //for (var key in cfg) {

            //    if (key != 'onBeforeTypeDefined' &&
            //        key != 'onTypeDefined' &&
            //        key != 'construct' &&
            //        key != 'initialize' &&
            //        key != 'base' &&
            //        key != 'mixins') {
            //        // The abstract array must be assigned to the prototype so it knows what are the abstract members that need to be implemented

            //        constructor.prototype[key] = cfg[key];
            //    }
            //}


            // If static then create an instance
            if (cfg.internal.static) {

                instance = new constructor();

                if (cfg.internal.onTypeDefined) { // Static initialization per module after the type is created (Do not call it here for non static types)
                    cfg.internal.onTypeDefined(instance);
                }
            }

            // Self register if a type is provided
            if (cfg.type) {

                typeManager.registerType(instance || constructor);
            }

            return instance || constructor;
        },

        // If the type (function construct) is already defined return it from the cache, otherwise add it to the cache and return it
        defineOnce: function (cfg) {
            var type = cfg.type,
                constructor;

            if (!type) {
                throw 'Type must be provided in the configuration';
            }

            constructor = typeManager.getType(type, true); // Optional

            if (constructor) { // It has been already loaded

                return constructor;
            }

            cfg.internal = cfg.internal || {};

            if (cfg.internal.onBeforeTypeDefined) { // Static initialization per module before the type is created

                cfg.internal.onBeforeTypeDefined();
            }

            constructor = this.define(cfg); // It self registers

            if (!cfg.internal.static && cfg.internal.onTypeDefined) { // Static initialization per module after the type is created (Do not call it here for static types)

                cfg.internal.onTypeDefined(constructor);
            }

            return constructor;
        },

        createEnumeration: function (cfg) {
            var o = {}

            for (var k in cfg) {

                o[k] = {
                    value: cfg[k],
                    writable: false,
                    enumerable: true
                };
            }

            return Object.defineProperties({}, o);
        },

        // Chains an existing function adding the functionality after calling the original one
        chain: function (first, second) {

            if (!first) {

                if (!second) {
                    //console.log('No functions to override .. returning empty function');
                    return function () { };
                }
                else {
                    return second;
                }
            }

            if (!second) {

                return first;
            }

            return function () {
                var args = Array.prototype.slice.call(arguments);

                first.apply(this, args);
                second.apply(this, args);
            };
        },

        chainQuitOnFalse: function (first, second) {

            if (!first) {

                if (!second) {
                    //console.log('No functions to override .. returning empty function');
                    return function () { };
                }
                else {
                    return second;
                }
            }

            if (!second) {

                return first;
            }

            return function () {
                var args = Array.prototype.slice.call(arguments);

                if (first.apply(this, args) === false) {

                    return false;
                }

                if (second.apply(this, args) === false) {

                    return false;
                }
            };

        }
    };
});